Skip to content

fix(messages): plain copy for failed uploads and a blocked Send - #3097

Open
matt2e wants to merge 2 commits into
errors/user-copy-messagesfrom
errors/upload-tile
Open

matt2e wants to merge 2 commits into
errors/user-copy-messagesfrom
errors/upload-tile

Conversation

@matt2e

@matt2e matt2e commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Summary

A failed attachment upload showed the browser's own text in the tile ("Failed to fetch", "signal timed out"), and a blocked Send gave no reason. Upload failures now map to plain copy through the shared helper. Send explains why it is blocked, and the failed tile's spacing is tidied.

Stacked on #3096: this PR targets errors/user-copy-messages (PR #3096), which adds the shared userCopy/failureKind helper used here. Review and merge #3096 first.

What changed

Upload failure copy (attachment-draft.ts, ComposerAttachments.tsx)

Case Before After
Lost connection (fetch TypeError) Failed to fetch Upload didn’t finish. Check your connection, then retry.
Five-minute upload timeout (TimeoutError) signal timed out Upload took too long. Retry, or remove this file.
Anything else the Error's own message Upload failed. Retry or remove this file.
Tile fallback with no message Upload failed. Retry or remove it to send. Upload failed. Retry or remove this file. (one shared fallback)
UploadError its mapped message unchanged

Blocked Send (MessageComposer.tsx)

  • Before: Send was disabled, with no tooltip.
  • After: while a failed upload blocks it, Send stays focusable (aria-disabled="true") with the tooltip Retry or remove the failed file to send. Clicking Send or pressing Enter still sends, publishes and uploads nothing.

Failed tile layout (ComposerAttachments.module.css)

  • The buttons keep base's order: Retry, then Remove.
  • The error and the button row each span the full tile width from its inner edge. The icon gap stays the resting tile's 14px, so the file name doesn't shift on failure.
  • The error sits 12px below the file row, and the buttons 12px below the error, 8px apart.
  • The buttons never wrap. The tile's min-content width keeps room for both, so a strip too narrow for them scrolls sideways instead of stacking them.

Before / After

failed upload tile at 1280px, base vs now

Final tile at 1280px: Failed to fetch becomes Upload didn’t finish. Check your connection, then retry. The error spans the tile and sits 12px from the file row and from the buttons. Retry still comes before Remove, as on base.

hovering Send while the failed file blocks it

Hovering Send while the failed file blocks it: the tooltip explains why Send does nothing. Older capture (round 3): the tile there shows Remove before Retry and the earlier spacing. The shipped tile is Retry, then Remove, as in the first image.

an upload that hits its timeout

An upload that hits its timeout: the DOMException's signal timed out becomes Upload took too long. Retry, or remove this file. Older capture (round 1): the copy is current, but the tile spacing predates the final layout.

Checks

  • pnpm typecheck: clean on this branch (on top of fix(errors): plain copy for failed history reads and connections #3096).
  • pnpm lint: exit 0, with 30 infos that predate this change. biome format is clean on the changed files.
  • Tests were not run for this PR (the workstation was under load). The changed unit tests and tests/browser/composer-attachments.spec.mjs are included but not run on this snapshot. On the source branch, a separate browser capture at 1280px measured the final tile: 12px file row to error, 12px error to buttons, and Retry and Remove sharing a top.

Known gaps

  • An attachment strip narrower than about 182px scrolls sideways rather than stacking Retry and Remove.

🤖 Generated with Claude Code

@matt2e
matt2e marked this pull request as ready for review November 9, 2026 15:39
@matt2e
matt2e requested a review from a team as a code owner November 9, 2026 15:39

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 One preparation-error recovery regression found.

@matt2e
matt2e force-pushed the errors/user-copy-messages branch from 4b2e055 to 32c31dc Compare November 9, 2026 16:06
@matt2e
matt2e force-pushed the errors/upload-tile branch from 9f4219b to e34a270 Compare November 9, 2026 16:06
matt2e and others added 2 commits November 13, 2026 10:49
…d Send

A failed attachment upload showed the Error's own message, such as
"Failed to fetch" or "signal timed out". attachment-draft.ts now maps
it by type with userCopy's failureKind:
- UploadError keeps its message, which is already written for people
- a lost connection: "Upload didn’t finish. Check your connection, then
  retry."
- the five-minute timeout: "Upload took too long. Retry, or remove this
  file."
- anything else: "Upload failed. Retry or remove this file.", now the
  one fallback shared with ComposerAttachments (which said "Upload
  failed. Retry or remove it to send.")

While a failed upload blocks it, Send stays focusable (aria-disabled)
with the tooltip "Retry or remove the failed file to send.", and Send
and Enter still send nothing.

The failed tile keeps Retry, then Remove, as on base. The error and the
actions each take a full row from the tile's inner edge; the error sits
12px below the file row and the actions 12px below the error, 8px
apart. The actions never wrap: the tile's min-content width keeps room
for both, so a strip too narrow for them scrolls instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
Choosing an image over 25 million pixels showed "Upload failed. Retry
or remove this file." instead of why. prepareAttachment threw plain
Errors for its own refusals, and the tile's mapper words any unknown
error generically.

prepare-attachment.ts now throws a PreparationError for the three
refusals it writes for people: the pixel limit, no canvas context, and
an image format the browser cannot prepare. The draft shows that
message as it does an UploadError's. Every other failure, including an
image the browser cannot decode, a canvas that yields no blob, and the
WebP worker's own text, keeps the generic copy.

A draft-level test runs the real preparation with a stubbed bitmap and
canvas context. On the previous commit it fails for the pixel limit and
the missing context; it also pins a decode error to the generic text.

Signed-off-by: mtoohey <mtoohey@squareup.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matt2e
matt2e force-pushed the errors/user-copy-messages branch from 32c31dc to 0e35fbc Compare November 12, 2026 23:50
@matt2e
matt2e force-pushed the errors/upload-tile branch from 82e6257 to 8f05fbe Compare November 12, 2026 23:50

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants